dofile([[../lib_simc_lua.lua]])

local header = [[
../Base.simc

report_details=0

gear_expertise_rating=2551
gear_hit_rating=2551

default_actions=1

html=
reforge_plot_output_file=
]]

orig_print = print

local function print(msg, ...)
	orig_print(msg:format(...))
end

local sys_output, num_cores
if isWindows() then
	sys_output = io.popen("WMIC CPU Get NumberOfCores,NumberOfLogicalProcessors /Format:List"):read("*all")
	num_cores = tonumber(sys_output:match("NumberOfLogicalProcessors=(%d+)"))
else
	sys_output = io.popen("grep -c ^processor /proc/cpuinfo", "r"):read("*all")
	num_cores = tonumber(sys_output) + 1
end
if num_cores and num_cores >= 0 then
	log("Detected %d cores", num_cores)
	header = header .. ("\nthreads=%d\n"):format(num_cores)
else
	log("Unable to retrieve number of cores: %s", tostring(sys_output))
end

function check_interruption()
	if prev_check_interruption and os.time() - prev_check_interruption < 10 then
		return
	end
	prev_check_interruption = os.time()
	local stop_flag = io.open("STOP")
	if stop_flag then
		print("Operation interrputed.")
		stop_flag:close()
		save_session(true)
		os.remove("STOP")
		os.exit()
	end
end

function new_node( crit, haste )
	local mastery = session.total_stat - crit - haste
	if crit < session.min_stat or haste < session.min_stat or mastery < session.min_stat then
		return
	end
	return {
		crit = crit,
		haste = haste,
		mastery = mastery,
		iterations = 0,
	}
end

function node_sig( node )
	return node.crit .. "-" .. node.haste
end

function fetch_rating( crit, haste )
	local node = session.nodes[crit .. "-" .. haste]
	if not node then
		node = new_node(crit, haste)
		session.nodes[crit .. "-" .. haste] = node
	end
	return node
end

function compare_nodes( base_node, node, err_multiplier )
	if not node.dps then
		return
	end
	local base_min = base_node.dps - base_node.err * err_multiplier
	local base_max = base_node.dps + base_node.err * err_multiplier
	local node_min = node.dps - node.err * err_multiplier
	local node_max = node.dps + node.err * err_multiplier
	if base_node.iterations > 0 then
		if node_min > base_max then
			return "EXT_HIGH"
		elseif node_max < base_min then
			return "EXT_LOW"
		end
	else
		return
	end
end

function combine_nodes( base_node, add_node )
	if not add_node or not add_node.dps then
		return base_node
	end
	assert(base_node.crit == add_node.crit)
	assert(base_node.haste == add_node.haste)
	if not base_node.dps then
		base_node.dps = add_node.dps
		base_node.iterations = add_node.iterations
		base_node.err = add_node.err
	else
		print("\tCombine node1 (%d, %d) : %d - %d @%d", base_node.crit, base_node.haste, base_node.dps, base_node.err, base_node.iterations)
		print("\tCombine node2 (%d, %d) : %d - %d @%d", add_node.crit, add_node.haste, add_node.dps, add_node.err, add_node.iterations)
		local iterations =  base_node.iterations + add_node.iterations
		base_node.dps = (base_node.dps * base_node.iterations + add_node.dps * add_node.iterations) / iterations
		base_node.err = math.sqrt((base_node.err * base_node.iterations) ^ 2 + (add_node.err * add_node.iterations) ^ 2) / iterations
		base_node.iterations = iterations
		print("\tCombine result (%d, %d) : %d - %d @%d", base_node.crit, base_node.haste, base_node.dps, base_node.err, base_node.iterations)
	end
	return base_node
end

function do_rating( node, iterations )
	check_interruption()

	local iterations_required = iterations - node.iterations
	log("\tRating node (%d, %d) @ %d, need %d, got %d", node.crit, node.haste, iterations_required, iterations, node.iterations)
	session.iterations = iterations_required
	local add_node = new_node(node.crit, node.haste)
	add_node.profile = ("gear_crit_rating=%d\ngear_haste_rating=%d\ngear_mastery_rating=%d\n"):format(
		add_node.crit,
		add_node.haste,
		add_node.mastery
	)
	add_node.iterations = iterations_required
	local elapsed, simcElapsed = process_profile(add_node)
--	if elapsed > 0 then
--		log("\t\tSim #%d took time %.3f / %.3f (%.2f%%)", #session.profiles + 1, simcElapsed, elapsed, simcElapsed / elapsed * 100)
--	end
	session.nodes[node_sig(node)] = combine_nodes(node, add_node)
	log("\t\tRating (%d, %d) : %d - %d @%d", node.crit, node.haste, node.dps, node.err, node.iterations)
	save_session()
	return node
end

function try_climb( crit, haste, max_dist, iterations, err_multiplier )
	check_interruption()

	stage_index = stage_index + 1
	log("Try climb %s from (%d, %d) @%d max dist %d, stage %d", session.name, crit, haste, iterations, max_dist, stage_index)
	if isWindows() and not fast_mode then
		os.execute(title_base .. ("(%d, %d) +%d @%d stage %d - %d hops"):format(crit, haste, max_dist, iterations, stage_index, #session.hops))	-- Fast mode
	end
	local base_node = fetch_rating(crit, haste)
	if not base_node.dps or base_node.iterations < iterations then
		do_rating(base_node, iterations)
	end

	local neighbours = {}

	local max_dist_sq = max_dist ^ 2
	for crit_i = -max_dist, max_dist do
		local crit_d = crit_i * session.delta + crit
		for haste_i = -max_dist, max_dist do
			local haste_d = haste_i * session.delta + haste
			if not (crit_i == 0 and haste_i == 0) then
				local node = fetch_rating(crit_d, haste_d)
				if node then
					local dist_sq = (crit_i ^ 2) + (haste_i ^ 2)
					if dist_sq <= max_dist_sq then
						table.insert(neighbours, node)
					end
				end
			end
		end
	end

--	table.sort( neighbours, function (node1, node2)
--		return node1.dist < node2.dist
--	end)
	log("Start processing %d neighbour nodes", #neighbours)
	for index, node in ipairs(neighbours) do
		local status = compare_nodes(base_node, node, err_multiplier)
		if not node.dps then
			log("Processing neighbour %d / %d: Init node", index, #neighbours)
			do_rating(node, iterations)
		elseif node.iterations >= iterations then

		elseif status == "EXT_LOW" then

		else
			log("Processing neighbour %d / %d: Increment node %d -> %d", index, #neighbours, node.iterations, iterations)
			do_rating(node, iterations)
		end
		-- if not node.dps or node.iterations < iterations and status ~= "EXT_LOW" then
		-- 	do_rating(node, iterations)
		-- end
	end

	table.sort( neighbours, function (node1, node2)
		return node1.dps - node1.err > node2.dps - node2.err
	end)

	for index, node in ipairs(neighbours) do
		local status = compare_nodes(base_node, node, err_multiplier)
		if status == "EXT_HIGH" then
			log("=============Found EXT_HIGH on (%d, %d)==========================", node.crit, node.haste)
			return node
		end
	end
end

function climb_surf( crit, haste )
	session.hops = {}
	local node = fetch_rating(crit, haste)
	repeat
		stage_index = 0
		local next_node =
			try_climb(node.crit, node.haste, 1, 2500,	0.7) or
			try_climb(node.crit, node.haste, 2, 2500,	0.7) or
			try_climb(node.crit, node.haste, 3, 2500,	0.7) or
			try_climb(node.crit, node.haste, 4, 2500,	0.7) or
			try_climb(node.crit, node.haste, 5, 2500,	0.7) or
			try_climb(node.crit, node.haste, 6, 2500,	0.7) or
			try_climb(node.crit, node.haste, 7, 2500,	0.7) or
			try_climb(node.crit, node.haste, 1, 5000,	0.7) or
			try_climb(node.crit, node.haste, 2, 5000,	0.7) or
			try_climb(node.crit, node.haste, 3, 5000,	0.7) or
			try_climb(node.crit, node.haste, 4, 5000,	0.7) or
			try_climb(node.crit, node.haste, 5, 5000,	0.7) or
			try_climb(node.crit, node.haste, 6, 5000,	0.7) or
			try_climb(node.crit, node.haste, 1, 10000,	0.7) or
			try_climb(node.crit, node.haste, 2, 10000,	0.7) or
			try_climb(node.crit, node.haste, 3, 10000,	0.7) or
			try_climb(node.crit, node.haste, 4, 10000,	0.7) or
			try_climb(node.crit, node.haste, 5, 10000,	0.7) or
			try_climb(node.crit, node.haste, 6, 10000,	0.7) or
			try_climb(node.crit, node.haste, 1, 20000,	0.7) or
			try_climb(node.crit, node.haste, 2, 20000,	0.7) or
			try_climb(node.crit, node.haste, 3, 20000,	0.7) or
			try_climb(node.crit, node.haste, 4, 20000,	0.7) or
			try_climb(node.crit, node.haste, 5, 20000,	0.7) or
			-- try_climb(node.crit, node.haste, 0, 100000,	0.7) or
			try_climb(node.crit, node.haste, 1, 40000,	0.7) or
			try_climb(node.crit, node.haste, 2, 40000,	0.7) or
			try_climb(node.crit, node.haste, 3, 40000,	0.7) or
			nil
		save_session(true)
		if next_node then
			node = next_node
			table.insert(session.hops, next_node)
		end
	until ( not next_node )
	log("=== Finished at (%d, %d) ===", node.crit, node.haste)
	return node
end


function process_session()
	log("Start processing session %s", session.name)
	if fast_mode then
		print("Processing %s", session.name)	-- Fast mode
	end

	title_base = ("title %s - "):format(session.name)

	session.header = header
	if session.total_agility then
		session.header = session.header .. ("\ngear_agility=%d\n"):format(session.total_agility)
		log("Using agility %d", session.total_agility)
	end

	if session.weapon_dps and session.weapon_range then
		local weapon_range = session.weapon_range / 100
		log("Using weapon dps %d at range %.4f", session.weapon_dps, weapon_range)
		local new_weapon_min = session.weapon_dps * (1 - weapon_range)
		local new_weapon_max = session.weapon_dps * (1 + weapon_range)
		if session.weapon_min ~= new_weapon_min then
			log("Overriding weapon_min %d with %d", session.weapon_min, new_weapon_min)
		end
		if session.weapon_max ~= new_weapon_max then
			log("Overriding weapon_max %d with %d", session.weapon_max, new_weapon_max)
		end
		session.weapon_min = new_weapon_min
		session.weapon_max = new_weapon_max
	end

	if session.weapon_min and session.weapon_max then
		session.header = session.header .. ("\nmain_hand=scaled_weapon,weapon=bow_3.00speed_%dmin_%dmax,enchant=lord_blastingtons_scope_of_doom\n"):format(session.weapon_min, session.weapon_max)
		log("Using weapon damage %d - %d", session.weapon_min, session.weapon_max)
	end

	if session.additional_profile then
		session.header = session.header .. "\n" .. session.additional_profile
	end

	local final_node = climb_surf(session.start_crit, session.start_haste, session.total_stat)

	session.total_iterations = 0
	log("Cleaning up zero nodes ...")
	for key, node in pairs(session.nodes) do
		if node.iterations == 0 then
			session.nodes[key] = nil
		else
			min_stat_reached = math.min(min_stat_reached, node.crit, node.haste)
			max_stat_reached = math.max(max_stat_reached, node.crit, node.haste)
		end
		session.total_iterations = session.total_iterations + node.iterations
	end
	session.final_crit = final_node.crit
	session.final_haste = final_node.haste

	log("Saving session ...")
	save_session(true)
	log("Finished processing session %s", session.name)
end

function process_base_group(base_info, add_info)
	local base_report_list = {}
	local combined_info = {}
	for key, value in pairs(base_info) do
		combined_info[key] = value
	end
	for key, value in pairs(add_info) do
		combined_info[key] = value
	end
	min_stat_reached, max_stat_reached = 1000000, 0
	for var = combined_info.var_end, combined_info.var_start, -combined_info.var_step do
		local info = {}
		for key, value in pairs(combined_info) do
			info[key] = value
		end
		log("Iterate %s from %s to %s step %s: %s", info.var_name, tostring(info.var_start), tostring(info.var_end), tostring(info.var_step), tostring(var))
		info[info.var_name] = var
		if type(info.func) then
			info.func(info)
		end
		info.name = info.name_pattern:format(var)
		info.fig_title = info.fig_title_pattern:format(var)
		session = {
			profiles = {},
			nodes = {},
		}
		local func = loadfile([[Sessions/]] .. info.name .. [[.lua]])
		if func then
			log("Loading session %s from file ...", info.name)
			func()
		end

		for key, value in pairs(info) do
			if session[key] ~= value then
				log("Warning: Overriding session %s info %s from %s to %s", info.name, key, tostring(session[key]), tostring(value))
			end
			session[key] = value
		end

		process_session()
		table.insert(base_report_list, session.name)
	end

	log("Generating report ...")
	local csv = io.open(csv_path, "a+")
	csv:write("Profile", "\t")
	csv:write("Agility", "\t")
	csv:write("Total Stat", "\t")
	csv:write("Crit", "\t")
	csv:write("Haste", "\t")
	csv:write("Mastery", "\t")
	csv:write("DPS", "\t")
	csv:write("Total Iterations", "\t")
	csv:write("Hops", "\t")
	-- csv:write("Start Crit", "\t")
	-- csv:write("Start Haste")
	csv:write("WDmg Min", "\t")
	csv:write("WDmg Max", "\t")
	csv:write("WDPS", "\t")
	csv:write("WDmg Range")
	csv:write("\t", "Image")
	csv:write("\n")
	csv:close()
	for index, name in ipairs(base_report_list) do
		log("Loading session %s", name)
		dofile([[Sessions/]] .. name .. [[.lua]])
		log("Writing report ...")
		report_base_profile(min_stat_reached, max_stat_reached)
	end
end

function report_base_profile(min_stat_reached, max_stat_reached)
	local final_node = fetch_rating(session.final_crit, session.final_haste)
	local csv = io.open(csv_path, "a+")
	csv:write(session.name, "\t")
	csv:write(session.total_agility, "\t")
	csv:write(session.total_stat, "\t")
	csv:write(final_node.crit, "\t")
	csv:write(final_node.haste, "\t")
	csv:write(final_node.mastery, "\t")
	csv:write(math.floor(final_node.dps), "\t")
	csv:write(session.total_iterations, "\t")
	csv:write(#session.hops, "\t")
	-- csv:write(session.start_crit, "\t")
	-- csv:write(session.start_haste)
	csv:write(session.weapon_min, "\t")
	csv:write(session.weapon_max, "\t")
	csv:write(session.weapon_dps or (session.weapon_min + session.weapon_max) / 2, "\t")
	if session.weapon_range then
		csv:write(string.format("%.1f", session.weapon_range), "%")
	else
		csv:write(string.format("%.1f", (session.weapon_max - session.weapon_min) / (session.weapon_max + session.weapon_min) * 100), "%")
	end
	csv:write("\t", ImageUrl[session.name] or "")

	local count_valid_pos = 0
	for crit = session.min_stat, session.total_stat, session.delta do
		for haste = session.min_stat, session.total_stat, session.delta do
			local mastery = session.total_stat - crit - haste
			if mastery >= session.min_stat then
				count_valid_pos = count_valid_pos + 1
			end
		end
	end
	csv:write("\t", count_valid_pos)

	csv:write("\n")
	csv:close()
	local weapon_dps = (20523 + 38116) / 2  -- 29319
	local range = (38116 - weapon_dps) / weapon_dps -- 30%
	-- local csv = io.open([[Sessions\]] .. session.name .. [[.csv]], "w")
	local csv = io.open([[Sessions/result_full.csv]], "a+")
	local matlab = io.open([[Sessions/result_full.m]], "a+")
	-- csv:write("Label", "\t")
	-- csv:write("Crit", "\t")
	-- csv:write("Haste", "\t")
	-- csv:write("Status", "\t")
	-- csv:write("Confidence", "\t")
	-- csv:write("Iterations", "\t")
	-- csv:write("DPS", "\t")
	-- csv:write("Error", "\t")
	-- csv:write("\n")
	csv:write("[Sheet]", "\t", session.name, "\n")

	local function dump_node(node, status, label)
		csv:write(label or " ", "\t")
		csv:write(node.crit, "\t")
		csv:write(node.haste, "\t")
		csv:write(status, "\t")
		csv:write(node.iterations, "\t")
		if node.err then
			csv:write(1 / node.err, "\t")
		else
			csv:write(0, "\t")
		end
		csv:write(node.dps or "", "\t")
		csv:write(node.err or "")
		csv:write("\n")

		matlab:write("\t")
		matlab:write(node.crit, " ")
		matlab:write(node.haste, " ")
		matlab:write(node.iterations, " ")
		matlab:write(node.dps, " ")
		matlab:write(node.err, ",\n")
	end

	local used_nodes = {}

	matlab:write("Data_High = [\n")
	dump_node(final_node, "HIGHEST", session.total_stat)
	matlab:write("];", "\n")

	local hasHOP = nil
	matlab:write("Data_Hop = [\n")
	for index, node in ipairs(session.hops) do
		local node = fetch_rating(node.crit, node.haste)
		if node ~= final_node then
			dump_node(node, "HOP", index)
			hasHOP = true
		end
		used_nodes[node] = true
	end
	matlab:write("];", "\n")

	matlab:write("Data_Low = [\n")
	for key, node in pairs(session.nodes) do
		if not used_nodes[node] then
			local status = compare_nodes(final_node, node, 0.7)
			if status == "EXT_LOW" then
				dump_node(node, "LOW")
				used_nodes[node] = true
			end
		end
	end
	matlab:write("];", "\n")

	local hasEQ = nil
	matlab:write("Data_Eq = [\n")
	for key, node in pairs(session.nodes) do
		if not used_nodes[node] then
			hasEQ = true
			dump_node(node, "EQ")
		end
	end
	matlab:write("];", "\n")

	local min_lim = math.floor((min_stat_reached - 500) / 1000) * 1000
	local max_lim = math.ceil((max_stat_reached + 500) / 1000) * 1000

	matlab:write("output_file = '", session.name , ".png';", "\n")
	matlab:write("fig_title = '", session.fig_title , "';", "\n")
	matlab:write("total_stat = ", session.total_stat , ";", "\n")
	matlab:write("min_stat = ", session.min_stat , ";", "\n")
	matlab:write("min_lim = ", min_lim , ";", "\n")
	matlab:write("max_lim = ", max_lim , ";", "\n")
	matlab:write("make_figure;", "\n")
	matlab:write("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n")
	matlab:close()
	csv:close()
end

os.execute("mkdir Sessions")
csv_path = [[Sessions/result.csv]]

os.remove(csv_path)
os.remove([[Sessions/result_full.csv]])
os.remove([[Sessions/result_full.m]])
------------------------------------------------------------------------------
local talent_Default = "talents=http://cn.battle.net/wow/en/tool/talent-calculator#Ya!002110"
local talent_Fevor = "talents=http://cn.battle.net/wow/en/tool/talent-calculator#Ya!002010"
local talent_TotH = "talents=http://cn.battle.net/wow/en/tool/talent-calculator#Ya!002210"
local talent_Crows = "talents=http://cn.battle.net/wow/en/tool/talent-calculator#Ya!002100"
local talent_Barrage = "talents=http://cn.battle.net/wow/en/tool/talent-calculator#Ya!002112"
local talent_PS = "talents=http://cn.battle.net/wow/en/tool/talent-calculator#Ya!002111"
local gear_NoTrinket = talent_Default .. "\ntrinket1=haromms_talisman,id=105527,upgrade=2"
local gear_NoTier2 = talent_Default .. "\ntier16_2pc_melee=0\ntier16_4pc_melee=0"
local gear_NoTier4 = talent_Default .. "\ntier16_2pc_melee=1\ntier16_4pc_melee=0"

-- fast_mode = true

if fast_mode then
	function log() return end	-- Fast mode
end

local Hunter_BM_T16H_info = {
	func = function ( info )
		info.total_agility = info.total_agility or math.floor(5.521437321e-1 * info.total_stat + 12034.63797)
		info.weapon_min = info.weapon_min or math.floor(2.719477338e-1 * info.total_stat + 11013.53165)
		info.weapon_max = info.weapon_max or math.floor(5.050224581e-1 * info.total_stat + 20456.37468)
		info.name_pattern = info.name_pattern or "BM_" .. info.abbrev .. "_%d"
		info.fig_title_pattern = info.fig_title_pattern or "属性收益 - 野兽控制猎人 - " .. info.title_kw .. " - %d"
	end,
	min_stat = 1000,
	delta = 500,
	start_crit = 6000,
	start_haste = 6000,
	baseChar = "Hunter_BM_T16H",
	var_start = 16000,
	var_end = 36000,
	var_step = 500,
	var_name = "total_stat",
	additional_profile = talent_Default,
}
-- 		local total_agility = math.floor(7.423987777e-1 * total_stat + 6943.026585)	-- Rogue Assassination

dofile("ImageUrl.lua")

-- if isWindows() then
	process_base_group(Hunter_BM_T16H_info, {abbrev = "DB",		title_kw = "基准",												})
	process_base_group(Hunter_BM_T16H_info, {abbrev = "Fev",	title_kw = "热情", 			additional_profile = talent_Fevor	})
	process_base_group(Hunter_BM_T16H_info, {abbrev = "TotH",	title_kw = "狩猎刺激",		additional_profile = talent_TotH	})
	process_base_group(Hunter_BM_T16H_info, {abbrev = "Crow",	title_kw = "夺命黑鸦",		additional_profile = talent_Crows	})
	process_base_group(Hunter_BM_T16H_info, {abbrev = "NAoC",	title_kw = "无既定之天命",	additional_profile = gear_NoTrinket	})
	process_base_group(Hunter_BM_T16H_info, {abbrev = "4T16",	title_kw = "无4T16",		additional_profile = gear_NoTier4	})
	process_base_group(Hunter_BM_T16H_info, {abbrev = "2T16",	title_kw = "无2T16",		additional_profile = gear_NoTier2	})
	process_base_group(Hunter_BM_T16H_info, {abbrev = "Barr",	title_kw = "弹幕射击",		additional_profile = talent_Barrage	})
	process_base_group(Hunter_BM_T16H_info, {abbrev = "PowS",	title_kw = "强风射击",		additional_profile = talent_PS		})

	process_base_group(Hunter_BM_T16H_info, {abbrev = "Agi",	title_kw = "敏捷",			total_stat = 35000,
						var_start = 25000, var_end = 35000, var_name = "total_agility"})

	local weapon_dps = (20523 + 38116) / 2  -- 29319
	local range = (38116 - weapon_dps) / weapon_dps * 100 -- 30%
	process_base_group(Hunter_BM_T16H_info, {abbrev = "DPS",	title_kw = "武器DPS",		total_stat = 35000,
		var_start = 25000, var_end = 35000, var_name = "weapon_dps", weapon_range = 30})
	process_base_group(Hunter_BM_T16H_info, {abbrev = "Rng",	fig_title_pattern = "属性收益 - 野兽控制猎人 - 武器伤害离散度 - %d%%", total_stat = 35000,
		var_start = 20,	var_end = 40, var_step = 1, var_name = "weapon_range", weapon_dps = weapon_dps})
-- else
-- end
-- insert_base_profile("Rogue%d",		"Rogue_Assassination_T16H", 15000, 32000, 1000)
